Java IO学习-NIO 核心组件及基本概念
概述
从 JDK1.4 开始,Java 提供了一系列改进的输入/输出处理的新特性,被统称为 NIO(即New I/O,也可以称为 Non Blocking IO)。新增了许多用于处理输入输出的类,这些类都被放在 java.nio 包及子包下,并且对原 java.io 包中的很多类进行改写,新增了满足 NIO 的功能。NIO 采用内存映射文件的方式来处理输入输出,NIO 将文件或文件的一段区域映射到内存中,这样就可以像访问内存一样 访问文件了。
Java NIO(New IO) 是从 Java 1.4 版本开始引入的一个新的 IO API,可以替代标准的 Java IO API。NIO 与原来的 IO 有同样的作用和目的,但是使用的方式完全不同, NIO 支持面向缓冲区的、基于通道的 IO 操作。 NIO 将以更加高效的方式进行文件的读写操作。
下面前面的几节主要介绍它和传统的 BIO 有啥子区别
什么是 NIO、BIO、AIO
BIO (Blocking I/O):同步阻塞I/O模式。 NIO (New I/O):同步非阻塞模式。 AIO (Asynchronous I/O):异步非阻塞I/O模型。
什么是 BIO
BIO 全称是 Blocking IO,是 JDK1.4 之前的传统 IO 模型,本身是同步阻塞模式。

线程发起 IO 请求后,一直阻塞 IO,直到缓冲区数据就绪后,再进入下一步操作。针对网络通信都是一请求一应答的方式,虽然简化了上层的应用开发,但在性能和可靠性方面存在着巨大瓶颈,试想一下如果每个请求都需要新建一个线程来专门处理,那么在高并发的场景下,机器资源很快就会被耗尽。
什么是 NIO
NIO 也叫 Non-Blocking IO 是同步非阻塞的 IO 模型。线程发起 IO 请求后,立即返回(非阻塞 IO)。
同步指的是必须等待 IO 缓冲区内的数据就绪,而非阻塞指的是,用户线程不原地等待 IO 缓冲区,可以先做一些其他操作,但是要定时轮询检查 IO 缓冲区数据是否就绪。Java 中的 NIO 是 new IO 的意思。
NIO 主要有 buffer、channel、selector 三种技术的整合,通过零拷贝的 buffer 取得数据,每一个客户端通过 channel 在 selector(多路复用器)上进行注册。服务端不断轮询 channel 来获取客户端的信息。

channel 上有 connect(连接)、accept(阻塞)、read(可读)、write(可写)四种状态标识。根据标识来进行后续操作。所以一个服务端可接收无限多的 channel。不需要新开一个线程。大大提升了性能。
什么是 AIO
AIO 即 Asynchronous I/O(异步 I/O),这是 Java 1.7 引入的 NIO 2.0 中用到的。整个过程中,用户线程发起一个系统调用之后无须等待,可以处理别的事情。由操作系统等待接收内容,接收后把数据拷贝到用户进程中,最后通知用户程序已经可以使用数据了,两个阶段都是非阻塞的。AIO 整个过程如下图:

AIO属于异步模型, 用户线程可以同时处理别的事情,我们怎么进一步加工处理结果呢? Java 在这个模型中提供了两种方法:
- 一种是基于“回调”,可以实现 CompletionHandler 接口,在调用时把回调函数传递给对应的 API 即可
- 另一种是返回一个 Future。处理完别的事情,可以通过
isDone()可查看是否已经准备好数据,通过get()方法等待返回数据。
传统的 BIO 流模型
参考资料 Java I/O体系从原理到应用,这一篇全说清楚了 参考资料 15.2.2 流的概念模型
BIO 全称是 Blocking IO,是 JDK1.4 之前的传统 IO 模型,就是同步阻塞 IO
Java 把所有设备里的有序数据抽象成流模型,简化了输入/输岀处理,理解了流的概念模型也就了解了 Java IO
计算机中的数据是基于随着时间变换高低电压信号传输的,这些数据信号连续不断(诸如计算机中存储的视频、音频、文件等,底层都是一串串的 0 和 1),有着固定的传输方向,类似水管中水的流动,因此抽象数据流(I/O流)的概念:指一组有顺序的、有起点和终点的字节集合

抽象出数据流的作用:实现程序逻辑与底层硬件解耦,通过引入数据流作为程序与硬件设备之间的抽象层,面向通用的数据流输入输出接口编程,而不是具体硬件特性,程序和底层硬件可以独立灵活替换和扩展
- InputStream、Reader:所有输入流的基类,前者是字节输入流,后者是字符输入流。
- OutputStream、Writer:所有输出流的基类, 前者是字节输出流,后者是字符输出流。
输入流模型
对于 InputStream 和 Reader 而言,它们把输入设备抽象成一个 “水管”,这个水管里的每个 “水滴依次排列”

字节流和字符流的处理方式其实非常相似,只是它们处理的输入输出单位不同而已。输入流使用隐式的记录指针来表示当前正准备从哪个 “水滴” 开始读取,每当程序从 InputStream 或 Reader 里取出一个或多个“水滴”后,记录指针自动向后移动;除此之外,InputStream 和 Reader 里都提供一些方法来控制记录指针的移动。
输出流模型
对于 OutputStream 和 Writer 而言,它们同样把输出设备抽象成一个“水管”,只是这个水管里没有任何水滴

当执行输出时,程序相当于依次把“水滴”放入到输出流的水管中,输出流同样采用隐式的记录指针来标识当前水滴即将放入的位置,每当程序向 OutputStream 或 Writer 里输出或多个水滴后,记录指针自动向后移动。
NIO 和 IO有什么区别
| IO | NIO |
|---|---|
| 面向流编程 | 面向块(缓冲区)编程 |
| 阻塞 IO | 非阻塞 IO |
| 无 | 选择器 selector |
NIO 主要有三大核心部分:Channel(通道),Buffer(缓冲区), Selector。传统 IO 基于字节流和字符流进行操作,而 NIO 基于 Channel 和 Buffer(缓冲区)进行操作,数据总是从通道读取到缓冲区中,或者从缓冲区写入到通道中。
NIO 和传统 IO(一下简称IO)之间第一个最大的区别是,IO 是面向流的,NIO 是面向缓冲区的。 Java IO 面向流意味着每次从流中读一个或多个字节,直至读取所有字节,它们没有被缓存在任何地方。此外,它不能前后移动流中的数据。如果需要前后移动从流中读取的数据,需要先将它缓存到一个缓冲区。
BIO 阻塞机制是怎样的?
BIO 阻塞机制:BIO 中 客户端 与 服务器端 进行交互,需要阻塞等待服务器的响应,服务器在建立连接后,也需要阻塞等待客户端的后续数据。
那 NIO 是如何非阻塞的?
NIO 非阻塞机制:客户端请求服务器端后,将请求数据写入服务器端的 缓冲区(Buffer)中,服务器端通过 选择器(Selector)轮询 通道(Channel),查询 缓冲区(Buffer)中是否有请求数据,客户端不用阻塞等待服务器端响应,服务器端也不用阻塞等待客户端的请求,因此这里实现了非阻塞机制。
说白了就是一个事件模型(观察者模式)
NIO 的三大组件
这一节先简单的概述 NIO 的三大组件它们之间的作用,具体的细节看下面
Java NIO 系统传输的核心在于:通道(Channel)和缓冲区(Buffer)。
这两者的关系:若需要使用 NIO 系统,首先需要获取用于连接 IO 设备的通道,以及用于容纳数据的缓冲区,然后操作缓冲区,对数据进行处理
注:通道表示打开 IO 设备(例如:文件、套接字)的连接。
而调度这些通道(Channel)就是通过选择器 Selector 来完成,它会根据客户端请求,选择指定的通道(Channel)为客户端进行服务

简而言之,通道负责传输,缓冲区负责存储
Channel 通道
它代表一个到实体(如一个硬件设备、一个文件、一个网络套接字或者一个能够执行一个或者多个不同的 IO 操作的程序组件)的开放连接,如读操作和写操作。
目前,可以把 Channel 看作是传入(入站)或者传出(出站)数据的载体。因此,它可以被打开或者被关闭,连接或者断开连接。
常见的 Channel 有以下四种,其中 FileChannel 主要用于文件传输,其余三种用于网络通信
- FileChannel
- DatagramChannel
- SocketChannel
- ServerSocketChannel
Selector 选择器
Selector(选择器)也是 Java NIO 中的一个组件,用于检查一个 或多个 NIO Channel 的状态是否处于可读、可写。如此可以实现单线程管理多个 Channels,也就是可以管理多个网络链接。
在使用 Selector 之前,处理 socket 连接还有以下两种方法
使用多线程技术
为每个连接分别开辟一个线程,分别去处理对应的 socket 连接

这种方法存在以下几个问题
1、内存占用高:每个线程都需要占用一定的内存,当连接较多时,会开辟大量线程,导致占用大量内存 2、线程上下文切换成本高 3、只适合连接数少的场景:连接数过多,会导致创建很多线程,从而出现问题
使用线程池技术
这种方式实际就是上面那种方式的改良版

这种方法依旧存在以下几个问题
阻塞模式下,线程仅能处理一个连接
- 线程池中的线程获取任务(task)后,只有当其执行完任务之后(断开连接后),才会去获取并执行下一个任务
- 若 socket 连接一直未断开,则其对应的线程无法处理其他 socket 连接
所以它更适用于 短连接场景
短连接即建立连接发送请求并响应后就立即断开,使得线程池中的线程可以快速处理其他连接
使用 Selector
了解了上面的两种传统维持多连接的局限性后,现在开始谈谈 NIO 的核心概念 selector(选择器)
selector 的作用就是配合一个线程来管理多个 channel(fileChannel 因为是阻塞式的,所以无法使用 selector),获取这些 channel 上发生的事件,这些 channel 工作在 非阻塞模式下,当一个 channel 中没有执行任务时,可以去执行其他 channel 中的任务。
它适合连接数多,但流量较少的场景

若事件未就绪,调用 selector 的 select() 方法阻塞线程,直到 channel 发生了就绪事件。这些事件就绪后,select 方法就会返回这些事件交给 thread 来处理
Buffer 缓冲区
学习之前要理解 Buffer 是一个用来装数据的容器
Buffer 有以下几种,其中使用较多的是 ByteBuffer
- ByteBuffer
- MappedByteBuffer
- DirectByteBuffer
- HeapByteBuffer
- ShortBuffer
- IntBuffer
- LongBuffer
- FloatBuffer
- DoubleBuffer
- CharBuffer
它们之间的继承关系如下:
